import os
import time
import json
import serial
import asyncio
import datetime
import subprocess
import numpy as np
from pathlib import Path
from gpiozero import Servo


PHOTOS = Path(__file__).parent / 'static' / 'photos'

DEBUG = True

def debug(msg):
    if not DEBUG: return
    print(msg)


class FilePipe:
    def __init__(self, path: Path=None):
        if not path:
            path = Path(__file__).parent / 'pipefile'
        self.path = path
    
    def get(self, key=None):
        if not self.path.exists():
            if key:
                return None
            else:
                return {}
        j = self.json_load()
        if key:
            return j.get(key)
        else:
            return j
    
    def json_load(self):
        while True:
            try:
                with self.path.open('r', encoding='utf-8') as fr:
                    j = json.load(fr)
                return j
            except Exception as e:
                time.sleep(1)
                print(e)

    def json_write(self, j):
        while True:
            try:
                with self.path.open('w', encoding='utf-8') as fw:
                    json.dump(j, fw)
                return True
            except Exception as e:
                time.sleep(1)
                print(e)
    
    def set(self, key, value=None):
        info = self.get()
        if isinstance(key, dict):
            info.update(key)
        else:
            info[key] = value
        self.json_write(info)
        return info
    
    def get_last_weight(self):
        debug('get_last_weight')
        result = self.get()
        if result.get('action', '') == 'take':
            return int(result.get('dweight', 0))
        return 0

    def take(self, dweight):
        self.set({'action': 'take', 'dweight': dweight})

    def put(self, cellphone):
        self.set({'action': 'put', 'cellphone': cellphone})


class Client:
    def __init__(self, port, baudrate, door_servo_pin):
        self.photos = PHOTOS
        self.photos.mkdir(parents=True, exist_ok=True)
        self.fpipe = FilePipe()
        self.ser = serial.Serial(port, baudrate)
        self.weights = []
        self.WEIGHTS_LEN = 50
        self.STABLE_PART_LEN = 25
        self.TIMEOUT = 20
        self.last_stable_rec_time = time.time()
        self.door_servo = Servo(door_servo_pin)

    def is_take(self):
        return self.fpipe.get('action') == 'take'

    async def read_weight(self):
        while True:
            # debug('read_weight')
            # while not self.ser.readable():
            #     await asyncio.sleep(1)
            # while self.ser.readable():
            try:
                if self.ser.readable():
                    weight = self.ser.readline().decode('utf-8').strip()
                    if not weight:
                        continue
                    weight = int(weight)
                    # debug(f'weight: {weight}')
                    if len(self.weights) > self.WEIGHTS_LEN:
                        del self.weights[-1]
                    self.weights.insert(0, weight)
            except Exception as e:
                print(e)
            await asyncio.sleep(0.01)
    
    async def get_stable_weight(self):
        while True:
            for i in range(self.WEIGHTS_LEN - self.STABLE_PART_LEN):
                if np.var(self.weights[i: i + self.STABLE_PART_LEN]) < 2:
                    return int(np.mean(self.weights[i: i + self.STABLE_PART_LEN]))
            await asyncio.sleep(0.1)
    
    def take_camera(self, weight):
        date = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
        cellphone = self.fpipe.get('cellphone')
        name = f'{weight}_{date}_{cellphone}.jpg'
        path = self.photos / name
        subprocess.call(f'raspistill -o {path}', shell=True)
    
    async def put(self):
        debug('put')
        last_weight = await self.get_stable_weight()
        debug(f'weights: {self.weights}')
        debug(f'last weight: {last_weight}')
        self.open_door()
        open_time = time.time()
        # while True:
        while True:
            if time.time() - open_time > self.TIMEOUT:
                print('[ERROR] 太长时间未投递，自动关闭！')
                debug(f'weights: {self.weights}')
                break
            dweight = await self.get_stable_weight() - last_weight
            if dweight > 2:
                debug(f'weights: {self.weights}')
                debug(f'dweight: {dweight}')
                self.fpipe.set('action', 'wait')
                self.close_door()
                self.take_camera(dweight)
                return True
            await asyncio.sleep(0.1)
        self.fpipe.set('action', 'wait')
        self.close_door()
        return True

    async def take(self):
        debug('take')
        last_weight = await self.get_stable_weight()
        self.fpipe.take(0)
        while self.is_take():
            now_weight = await self.get_stable_weight()
            dweight = last_weight - now_weight
            last_weight = now_weight
            if dweight > 2:
                debug(f'weights: {self.weights}')
                debug(f'dweight: {dweight}')
                self.fpipe.take(dweight)
            await asyncio.sleep(0.1)
        for p in self.photos.glob('*.jpg'):
            os.remove(p)
    
    def open_door(self):
        debug('open door')
        self.door_servo.value = -0.9

    def close_door(self):
        debug('close door')
        self.door_servo.value = 0.9

    async def async_run(self):
        while True:
            try:
                if self.fpipe.get('action') == 'put':
                    await self.put()
                elif self.is_take():
                    await self.take()
            except Exception as e:
                print(e)
            await asyncio.sleep(0.1)

    def run(self):
        tasks = [
            self.read_weight(),
            self.async_run()
        ]
        loop = asyncio.get_event_loop()
        loop.run_until_complete(asyncio.gather(*tasks))

if __name__ == '__main__':
    client = Client('/dev/ttyACM0', 9600, 17)
    client.run()
